﻿using System;
using System.Collections;
using System.Windows.Forms;

using OpenTK;
using OpenTK.Input;

using MosCraft.Utilities;
using MosCraft.Utilities.Input;
using OpenTK.Graphics.OpenGL;

namespace MosCraft
{
    /// <summary>
    /// This class handles and moving object on the players
    /// screen should it be an item, moving block or mob
    /// This is the Base class and handles or the Core Functions
    /// </summary>
    public class Entity
    {
        //Constants

        //Movement and postion
        private World world;
        public Vector3d Position;
        public Vector3d prevPosition;
        public Vector3d Velocity;
        private float Pitch;
        private float Yaw;
        private float prevPitch;
        private float prevYaw;
        private Vector3d spawnPostion;

        //The AABB collision box
        public readonly AxisAlignedBB boundingBox;

        //The Camera to use
        private Camera camera;
        private float FOV;

        //General Public Properties
        private float Width;
        private float Height;
        private float ySize;
        private float yOffset;
        private float stepHeight;
        private bool onGround;
        private bool isJumping;
        private bool isCollided;
        private bool isCollidedHorizontally;
        private bool isCollidedVertically;
        private bool isAlive;
        private double blockReach;
        private int selectBlockId;

        //General Private Properties
        private int ticksExisted;
        private FastRandom random;

        /// <summary>
        /// Creates a new instance of an entity
        /// </summary>
        /// <param name="world">The world the Entity Exists in</param>
        public Entity(World world)
        {
            this.world = world;
            this.Velocity = Vector3d.Zero;
            this.Width = 0.6f;
            this.Height = 1.8f;
            this.ySize = 0.0F;
            this.yOffset = 0.0F;
            this.stepHeight = 0.5F;
            this.isJumping = false;
            this.isAlive = true;
            this.onGround = false;
            this.isCollided = false;
            this.isCollidedHorizontally = false;
            this.isCollidedVertically = false;
            this.ticksExisted = 0;
            this.boundingBox = AxisAlignedBB.getBoundingBox(0.0D, 0.0D, 0.0D, 0.0D, 0.0D, 0.0D);
            this.camera = new Camera();
            this.random = new FastRandom();
            this.selectBlockId = 1;

            Settings.Config.GetField("Display", "FOV", out FOV);
            Settings.Config.GetField("Controls", "Reach", out blockReach);
            this.camera.setFov(FOV);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        public float getEyeHeight()
        {
            return this.Height * 0.85F;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <param name="z"></param>
        public void setPosition(double x, double y, double z)
        {
            Position = new Vector3d(x, y, z);

            float halfWidth = this.Width / 2.0F;

            boundingBox.setBounds(x - (double)halfWidth, (y - (double)yOffset) + (double)ySize, z - (double)halfWidth,
                x + (double)halfWidth, (y - (double)yOffset) + (double)ySize + (double)Height, z + (double)halfWidth);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="distance"></param>
        /// <param name="deltaTime"></param>
        /// <returns></returns>
        public RayTraceObject rayTrace(double distance)
        {
            Vector3d direction = camera.getPosition();
            Vector3d look = camera.getLookDirection();

            return world.rayTraceBlocks(direction, look, distance);
        }

        /// <summary>
        /// Updates the entities Logic
        /// Will be overidden by child classes
        /// </summary>
        /// <param name="GameTime">Ticks per frame</param>
        public virtual void onUpdate(double deltaTime)
        {
            camera.setPosition(Position.X, Position.Y + getEyeHeight(), Position.Z);
            camera.setRotation(Pitch, Yaw);

            prevPosition = Position;
            prevPitch = Pitch;
            prevYaw = Yaw;

            float moveForward = 0;
            float moveStrafing = 0;

            if (KeyboardHelper.IsKeyPress(Keys.W))
            {
                moveForward++;
            }

            if (KeyboardHelper.IsKeyPress(Keys.S))
            {
                moveForward--;
            }

            if (KeyboardHelper.IsKeyPress(Keys.D))
            {
                moveStrafing++;
            }

            if (KeyboardHelper.IsKeyPress(Keys.A))
            {
                moveStrafing--;
            }

            isJumping = KeyboardHelper.IsKeyPress(Keys.Space);

            moveStrafing *= 0.98F;
            moveForward *= 0.98F;

            if (isJumping)
            {
                if (this.onGround)
                {
                    Velocity.Y += 0.23D;
                }
            }

            this.setRoation(MouseHelper.Pitch, MouseHelper.Yaw);
            this.moveEntityWithHeading((float)moveForward, (float)moveStrafing, deltaTime);

            if (Position.Y < -64D)
            {
                this.kill();
            }

            for (; Yaw - prevYaw < -180F; prevYaw -= 360F) { }
            for (; Yaw - prevYaw >= 180F; prevYaw += 360F) { }
            for (; Pitch - prevPitch < -180F; prevPitch -= 360F) { }
            for (; Pitch - prevPitch >= 180F; prevPitch += 360F) { }

            pushOutOfBlocks(Position.X - (double)Width * 0.35D, boundingBox.min.Y + 0.5D, Position.Z + (double)Width * 0.35D);
            pushOutOfBlocks(Position.X - (double)Width * 0.35D, boundingBox.min.Y + 0.5D, Position.Z - (double)Width * 0.35D);
            pushOutOfBlocks(Position.X + (double)Width * 0.35D, boundingBox.min.Y + 0.5D, Position.Z - (double)Width * 0.35D);
            pushOutOfBlocks(Position.X + (double)Width * 0.35D, boundingBox.min.Y + 0.5D, Position.Z + (double)Width * 0.35D);

            this.ticksExisted++;
        }

        /// <summary>
        /// 
        /// </summary>
        private void kill()
        {
            this.isAlive = false;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="Forward"></param>
        /// <param name="Strafe"></param>
        /// <param name="deltaTime"></param>
        public void moveEntityWithHeading(double Forward, double Strafe, double deltaTime)
        {
            float friction = 3.91F;

            if (this.onGround)
            {
                friction = 0.54F;
            }

            //Convert Forward and strafe into Velocity
            this.moveEntityFlying(Forward, Strafe, 0.08F);

            //Apply the velocity to the Entities Postion
            this.moveEntity(Velocity);

            //Friction
            Velocity.X *= (double)friction * deltaTime;
            Velocity.Z *= (double)friction * deltaTime;

            //Gravity
            Velocity.Y -= 0.8D * deltaTime;
            Velocity.Y *= 0.98D;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="Forward"></param>
        /// <param name="Strafe"></param>
        /// <param name="movementSpeed"></param>
        public void moveEntityFlying(double Forward, double Strafe, double movementSpeed)
        {
            double deltaForward = movementSpeed * Forward;
            double deltaStrafe = movementSpeed * 0.8f * Strafe;

            Vector3d transformedForward = Vector3d.Transform(Vector3d.UnitX, Matrix4d.CreateRotationY(Yaw));
            Vector3d crossRight = Vector3d.Cross(transformedForward, Vector3d.UnitY);

            Velocity += (deltaForward * transformedForward) + (deltaStrafe * crossRight);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="Movement"></param>
        public void moveEntity(Vector3d Movement)
        {
            this.ySize *= 0.4F;

            Vector3d InitialVelocity = Movement;
            AxisAlignedBB axisalignedbb = boundingBox.copy();
            ArrayList AABBList = world.getCollidingBoundingBoxes(this, boundingBox.addCoord(Movement));

            ///
            /// Handle the Y-Axis
            ///   
            for (int i = 0; i < AABBList.Count; i++)
            {
                Movement.Y = ((AxisAlignedBB)AABBList[i]).calculateYOffset(boundingBox, Movement.Y);
            }

            boundingBox.offset(0.0D, Movement.Y, 0.0D);

            if (InitialVelocity.Y != Movement.Y)
            {
                Movement.Y = 0.0D;
            }

            ///
            /// Handle the X-Axis
            ///
            for (int i = 0; i < AABBList.Count; i++)
            {
                Movement.X = ((AxisAlignedBB)AABBList[i]).calculateXOffset(boundingBox, Movement.X);
            }

            boundingBox.offset(Movement.X, 0.0D, 0.0D);

            if (InitialVelocity.X != Movement.X)
            {
                Movement.X = 0.0D;
            }

            ///
            /// Handle the Z-Axis
            ///
            for (int i = 0; i < AABBList.Count; i++)
            {
                Movement.Z = ((AxisAlignedBB)AABBList[i]).calculateZOffset(boundingBox, Movement.Z);
            }

            boundingBox.offset(0.0D, 0.0D, Movement.Z);

            if (InitialVelocity.Z != Movement.Z)
            {
                Movement.Z = 0.0D;
            }

            Position.X = (boundingBox.min.X + boundingBox.max.X) / 2D;
            Position.Y = (boundingBox.min.Y + (double)yOffset) - (double)ySize;
            Position.Z = (boundingBox.min.Z + boundingBox.max.Z) / 2D;

            this.isCollidedHorizontally = !Double.Equals(Movement.X, InitialVelocity.X) || !Double.Equals(Movement.Z, InitialVelocity.Z);
            this.isCollidedVertically = !Double.Equals(InitialVelocity.Y, Movement.Y);
            this.isCollided = this.isCollidedHorizontally || this.isCollidedVertically;
            this.onGround = isCollidedVertically && InitialVelocity.Y < 0.0D;

            //Velocity is no longer changing so its reset
            if (!Double.Equals(Movement.X, Velocity.X))
            {
                Velocity.X = 0.0D;
            }

            if (!Double.Equals(Movement.Y, Velocity.Y))
            {
                Velocity.Y = 0.0D;
            }

            if (!Double.Equals(Movement.Z, Velocity.Z))
            {
                Velocity.Z = 0.0D;
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="yaw"></param>
        /// <param name="pitch"></param>
        public void setRoation(float pitch, float yaw)
        {
            this.Pitch = pitch % 360.0f;
            this.Yaw = yaw % 360.0f;   
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <param name="z"></param>
        /// <returns></returns>
        internal virtual bool pushOutOfBlocks(double x, double y, double z)
        {
            int intX = (int)Math.Floor(x);
            int intY = (int)Math.Floor(y);
            int intZ = (int)Math.Floor(z);

            double deltaX = x - (double)intX;
            double deltaY = y - (double)intY;
            double deltaZ = z - (double)intZ;

            if (world.isBlockNormalCube(intX, intY, intZ))
            {
                bool blockLeft = !world.isBlockNormalCube(intX - 1, intY, intZ);
                bool blockRight = !world.isBlockNormalCube(intX + 1, intY, intZ);
                bool blockBellow = !world.isBlockNormalCube(intX, intY - 1, intZ);
                bool blockAbove = !world.isBlockNormalCube(intX, intY + 1, intZ);
                bool blockBehind = !world.isBlockNormalCube(intX, intY, intZ - 1);
                bool blockFront = !world.isBlockNormalCube(intX, intY, intZ + 1);

                EntityDirection? direction = null;
                double adjustment = double.MaxValue;

                if (blockLeft && deltaX < adjustment)
                {
                    adjustment = deltaX;
                    direction = EntityDirection.North;
                }

                if (blockRight && 1.0D - deltaX < adjustment)
                {
                    adjustment = 1.0D - deltaX;
                    direction = EntityDirection.South;
                }

                if (blockBellow && deltaY < adjustment)
                {
                    adjustment = deltaY;
                    direction = EntityDirection.Down;
                }

                if (blockAbove && 1.0D - deltaY < adjustment)
                {
                    adjustment = 1.0D - deltaY;
                    direction = EntityDirection.Up;
                }

                if (blockBehind && deltaZ < adjustment)
                {
                    adjustment = deltaZ;
                    direction = EntityDirection.East;
                }

                if (blockFront && 1.0D - deltaZ < adjustment)
                {
                    double d7 = 1.0D - deltaZ;
                    direction = EntityDirection.West;
                }

                double motion = random.nextDouble() * 0.2D + 0.1D;

                if (direction.HasValue)
                {
                    if (direction == EntityDirection.North)
                    {
                        Velocity.X = -motion;
                    }

                    if (direction == EntityDirection.South)
                    {
                        Velocity.X = motion;
                    }

                    if (direction == EntityDirection.Down)
                    {
                        Velocity.Y = -motion;
                    }

                    if (direction == EntityDirection.Up)
                    {
                        Velocity.Y = motion;
                    }

                    if (direction == EntityDirection.East)
                    {
                        Velocity.Z = -motion;
                    }

                    if (direction == EntityDirection.West)
                    {
                        Velocity.Z = motion;
                    }
                }
                return true;
            }

            return false;
        }

        internal void mouseClick(MouseButton mouseButton)
        {
            RayTraceObject rayTraceBlock = this.rayTrace(this.blockReach);
            if (rayTraceBlock != null)
            {
                if (mouseButton == MouseButton.Left)
                {
                    world.setBlockID(rayTraceBlock.blockX, rayTraceBlock.blockY, rayTraceBlock.blockZ, 0);
                }

                if (mouseButton == MouseButton.Middle)
                {
                    selectBlockId = rayTraceBlock.ID;
                }

                if (mouseButton == MouseButton.Right)
                {
                    int x,y,z;
                    rayTraceBlock.ProjectBlockPlace(out x, out y, out z);

                    if (Block.blocksList[selectBlockId].canBlockStay(world, x, y, z))
                    {
                        world.setBlockID(x, y, z, selectBlockId);
                    }
                }
            }
        }

        public void renderRayCast()
        {
            RayTraceObject rayTraceBlock = this.rayTrace(this.blockReach);

            if (rayTraceBlock != null)
            {
                float expand = 0.002f;
                drawOutlinedBoundingBox(Block.blocksList[rayTraceBlock.ID]
                    .getSelectedBoundingBoxFromPool(world, rayTraceBlock.blockX, rayTraceBlock.blockY, rayTraceBlock.blockZ)
                    .expand(expand, expand, expand));
            }
        }

        private void drawOutlinedBoundingBox(AxisAlignedBB axisalignedbb)
        {
            GL.Disable(EnableCap.Texture2D);
            GL.Disable(EnableCap.CullFace);
            GL.Enable(EnableCap.Blend);
            GL.LineWidth(2f);
            GL.Enable(EnableCap.LineSmooth);

            GL.PolygonMode(MaterialFace.FrontAndBack, PolygonMode.Line);
            GL.Color4(0.0f, 0.0f, 0.0f, 0.3f);

            GL.Begin(BeginMode.Quads);
            //Top
            GL.Vertex3(axisalignedbb.min.X, axisalignedbb.min.Y, axisalignedbb.min.Z);
            GL.Vertex3(axisalignedbb.max.X, axisalignedbb.min.Y, axisalignedbb.min.Z);
            GL.Vertex3(axisalignedbb.max.X, axisalignedbb.min.Y, axisalignedbb.max.Z);
            GL.Vertex3(axisalignedbb.min.X, axisalignedbb.min.Y, axisalignedbb.max.Z);

            //bottom
            GL.Vertex3(axisalignedbb.min.X, axisalignedbb.max.Y, axisalignedbb.min.Z);
            GL.Vertex3(axisalignedbb.max.X, axisalignedbb.max.Y, axisalignedbb.min.Z);
            GL.Vertex3(axisalignedbb.max.X, axisalignedbb.max.Y, axisalignedbb.max.Z);
            GL.Vertex3(axisalignedbb.min.X, axisalignedbb.max.Y, axisalignedbb.max.Z);

            //sides
            GL.Vertex3(axisalignedbb.min.X, axisalignedbb.max.Y, axisalignedbb.min.Z);
            GL.Vertex3(axisalignedbb.min.X, axisalignedbb.min.Y, axisalignedbb.min.Z);
            GL.Vertex3(axisalignedbb.max.X, axisalignedbb.min.Y, axisalignedbb.min.Z);
            GL.Vertex3(axisalignedbb.max.X, axisalignedbb.max.Y, axisalignedbb.min.Z);

            GL.Vertex3(axisalignedbb.max.X, axisalignedbb.min.Y, axisalignedbb.max.Z);
            GL.Vertex3(axisalignedbb.max.X, axisalignedbb.max.Y, axisalignedbb.max.Z);
            GL.Vertex3(axisalignedbb.min.X, axisalignedbb.max.Y, axisalignedbb.max.Z);
            GL.Vertex3(axisalignedbb.min.X, axisalignedbb.min.Y, axisalignedbb.max.Z);

            GL.End();
            
            GL.PolygonMode(MaterialFace.FrontAndBack, PolygonMode.Fill);

            GL.Disable(EnableCap.LineSmooth);
            GL.Enable(EnableCap.Texture2D);
            GL.Disable(EnableCap.Blend);
            GL.Enable(EnableCap.CullFace);
        }

        public void renderGUI()
        {
            int crossSize = 7;
            int crossHole = 2;

            GL.Disable(EnableCap.Texture2D);
            GL.Color4(1.0f, 1.0f, 1.0f, 0.6f);
            GL.LineWidth(1.5f);
            GL.Begin(BeginMode.Lines);

            GL.Vertex3(MosCraft.clientWidth / 2 - crossSize - crossHole, MosCraft.clientHight / 2, 0);
            GL.Vertex3(MosCraft.clientWidth / 2 - crossHole, MosCraft.clientHight / 2, 0);

            GL.Vertex3(MosCraft.clientWidth / 2 + crossSize + crossHole, MosCraft.clientHight / 2, 0);
            GL.Vertex3(MosCraft.clientWidth / 2 + crossHole, MosCraft.clientHight / 2, 0);

            GL.Vertex3(MosCraft.clientWidth / 2, MosCraft.clientHight / 2 - crossSize - crossHole, 0);
            GL.Vertex3(MosCraft.clientWidth / 2, MosCraft.clientHight / 2 - crossHole, 0);

            GL.Vertex3(MosCraft.clientWidth / 2, MosCraft.clientHight / 2 + crossSize + crossHole, 0);
            GL.Vertex3(MosCraft.clientWidth / 2, MosCraft.clientHight / 2 + crossHole, 0);

            GL.End();
            GL.Enable(EnableCap.Texture2D);
        }

        /// <summary>
        /// Get the current chunk (X,Z)
        /// that the Entity is over
        /// </summary>
        /// <returns>ChunkCoordinate with the X,Z value</returns>
        public ChunkCoordinate getChunkOver()
        {
            int x = (int) this.Position.X;
            int z = (int) this.Position.Z;

            return new ChunkCoordinate(x >> 4, z >> 4);
        }

        /// <summary>
        /// Gets the Camera to look through
        /// </summary>
        /// <returns></returns>
        public Camera getFirstPersonCamera()
        {
            return camera;
        }

        /// <summary>
        /// Used as a debug tool
        /// </summary>
        /// <returns>String Information of Class</returns>
        public override string ToString()
        {
            return string.Format("X:{0} Y:{1} Z:{2} G:{3}", (float)Position.X, (float)Position.Y, (float)Position.Z, onGround);
        }
    }
}

